/* 静态资源-JS 检测策略 */
const traverse = require('@babel/traverse').default;
const CONSTS = require('@lib/consts/index');
const Report = require('@lib/utils/report');
const Fs = require('@lib/utils/fs');
const Js = require('@lib/utils/js');
const {
  isValuableArray,
  mergeArrayByField
} = require('@lib/utils/tools');

// 需要写入报告文件的数据
const outputList = [];

class SrJs {
  /**
   * 执行策略的函数
   * @param {*} jsList js文件列表
   * @param {*} extra 额外参数
   * @param {*} cb 回调函数
   */
  static async execute(jsList = [], extra, cb) {
    if (!isValuableArray(jsList)) {
      cb && cb();
      return;
    }
    // 1.按照文件遍历
    for (const item of jsList) {
      const { filePath } = item;
      const jsContent = Fs.readFileSync(filePath);
      // 2.策略开始
      SrJs.detectFreqDOMOpera(item, jsContent, outputList);
      SrJs.detectLoopAndIterations(item, jsContent, outputList);
      SrJs.detectEventListener(item, jsContent, outputList);
      SrJs.detectImportFrom(item, jsContent, outputList);
      // 以下策略都需要使用AST，所以需要先获取AST
      const ast = Js.getAstByJsCode(jsContent);
      SrJs.detectEvalUsage(item, ast, outputList);
      SrJs.detectAnimationCode(item, ast, outputList);
      SrJs.detectCommonJS(item, ast, outputList);
    }
    // 3.整合outputList相同title项
    const newOutputList = mergeArrayByField(outputList, 'title', 'data');
    // 4.输出数据到报告目录下的临时文件
    if (!newOutputList.length) {
      cb && cb();
      return;
    }
    const { index } = extra;
    Report.outputTempFile(
      newOutputList,
      '静态资源-JavaScript',
      `${index}sr-js-${CONSTS.REPORT_TEMP_FILENAME}`,
      () => {
        cb && cb();
      }
    );
  }

  /**
   * 策略1：检测是否存在频繁的 DOM 操作，这里检测是否使用innerHTML
   * @param {*} jsItem js列表每一项
   * @param {*} jsContent js文件内容
   * @param {*} outputList 报告文件的数据列表
   */
  static detectFreqDOMOpera(jsItem, jsContent, outputList = []) {
    const regex = /\binnerHTML\b/;
    if (regex.test(jsContent)) {
      Report.formatAndpushReportData(
        [{
          fileName: jsItem.fileName,
          filePath: jsItem.filePath
        }],
        '【!谨慎使用】存在使用innerHTML可能导致频繁操作DOM，视实际情况优化',
        outputList
      );
    }
  }

  /**
   * 策略2：检测是否存在不合理循环和迭代
   * @param {*} jsItem js列表每一项
   * @param {*} jsContent js文件内容
   * @param {*} outputList 报告文件的数据列表
   */
  static detectLoopAndIterations(jsItem, jsContent, outputList = []) {
    // 正则表达式匹配 for 循环
    const forLoopRegex = /\bfor\s*\([\s\S]*?\)\s*\{/g;
    // 正则表达式匹配 while 循环
    const whileLoopRegex = /\bwhile\s*\([\s\S]*?\)\s*\{/g;
    // 正则表达式匹配 do-while 循环
    const doWhileLoopRegex = /\bdo\s*\{[\s\S]*?\}\s*while\s*\([\s\S]*?\)\s*;/g;

    // 检测文件中是否存在不合理的循环和迭代结构
    const hasForLoop = forLoopRegex.test(jsContent);
    const hasWhileLoop = whileLoopRegex.test(jsContent);
    const hasDoWhileLoop = doWhileLoopRegex.test(jsContent);

    if (hasForLoop || hasWhileLoop || hasDoWhileLoop) {
      Report.formatAndpushReportData(
        [{
          fileName: jsItem.fileName,
          filePath: jsItem.filePath
        }],
        '【!谨慎使用】可能存在不合理的for/while/doWhile循环和迭代结构，视实际情况优化',
        outputList
      );
    }
  }

  /**
   * 策略3：检测是否存在过多的事件监听器，这里检测使用addEventListener的次数
   * @param {*} jsItem js列表每一项
   * @param {*} jsContent js文件内容
   * @param {*} outputList 报告文件的数据列表
   */
  static detectEventListener(jsItem, jsContent, outputList = []) {
    // 正则表达式匹配事件监听器绑定
    const eventListenerRegex = /\.addEventListener\(['"].+['"]\s*,/g;
    // 检测文件中是否存在过多的事件监听器
    const matches = jsContent.match(eventListenerRegex);
    const numberOfEventListeners = isValuableArray(matches) ? matches.length : 0;
    // 事件监听器超过允许的阈值，给出告警提示
    const maxEventListener = CONSTS.MAX_EVENT_LISTENER;
    if (numberOfEventListeners > maxEventListener) {
      Report.formatAndpushReportData(
        [{
          fileName: jsItem.fileName,
          filePath: jsItem.filePath
        }],
        `【!谨慎使用】使用超过${maxEventListener}个事件监听器addEventListener`,
        outputList
      );
    }
  }

  /**
   * 策略4：检测是否存在使用了ESModules模块import加载的方式但缺乏代码分割和按需加载，这里检测使用import-from方式导入
   * @param {*} jsItem js列表每一项
   * @param {*} jsContent js文件内容
   * @param {*} outputList 报告文件的数据列表
   */
  static detectImportFrom(jsItem, jsContent, outputList = []) {
    const regex = /\bimport\b.*\bfrom\b.*['"].*['"]\s*;/g;
    if (regex.test(jsContent)) {
      Report.formatAndpushReportData(
        [{
          fileName: jsItem.fileName,
          filePath: jsItem.filePath
        }],
        '【!谨慎使用】存在使用import加载但没有进行代码分割和按需加载，视实际情况优化',
        outputList
      );
    }
  }

  /**
   * 策略5：检测是否使用 eval 的函数
   * @param {*} jsItem js列表每一项
   * @param {*} ast AST抽象语法树
   * @param {*} outputList 报告文件的数据列表
   */
  static detectEvalUsage(jsItem, ast, outputList = []) {
    let evalUsed = false;
    // 遍历 AST，检测是否使用了 eval
    traverse(ast, {
      CallExpression(path) {
        if (Js.isEvalCall(path)) {
          evalUsed = true;
          // 停止遍历，因为已经发现了 eval 的使用
          path.stop();
        }
      }
    });
    if (evalUsed) {
      Report.formatAndpushReportData(
        [{
          fileName: jsItem.fileName,
          filePath: jsItem.filePath
        }],
        '【!谨慎使用】eval函数',
        outputList
      );
    }
  }

  /**
   * 策略6：检测是否使用 JavaScript API 绘制动画
   * @param {*} jsItem js列表每一项
   * @param {*} ast AST抽象语法树
   * @param {*} outputList 报告文件的数据列表
   */
  static detectAnimationCode(jsItem, ast, outputList = []) {
    let useAnimation = false;
    // 遍历 AST，检测是否使用了 JavaScript 绘制动画相关的 API
    traverse(ast, {
      Identifier(path) {
        if (Js.isAnimationAPIs(path)) {
          useAnimation = true;
          // 停止遍历，因为已经发现了相关绘制函数调用
          path.stop();
        }
      }
    });
    if (useAnimation) {
      Report.formatAndpushReportData(
        [{
          fileName: jsItem.fileName,
          filePath: jsItem.filePath
        }],
        '【!谨慎使用】JavaScript API(canvas/canvasrenderingcontext2d/svgelement/animation) 绘制动画',
        outputList
      );
    }
  }

  /**
   * 策略7：检测是否使用 CommonJS 模块加载，建议使用 ES Modules 模块加载代替
   * @param {*} jsItem js列表每一项
   * @param {*} ast AST抽象语法树
   * @param {*} outputList 报告文件的数据列表
   */
  static detectCommonJS(jsItem, ast, outputList = []) {
    let isCommonJS = false;
    // 遍历 AST，检测是否使用了 CommonJS 模块加载
    traverse(ast, {
      CallExpression(path) {
        if (
          path.node.callee.type === 'Identifier' &&
          path.node.callee.name === 'require'
        ) {
          isCommonJS = true;
          // 停止遍历，因为已经发现了 CommonJS 模块加载
          path.stop();
        }
      }
    });
    if (isCommonJS) {
      Report.formatAndpushReportData(
        [{
          fileName: jsItem.fileName,
          filePath: jsItem.filePath
        }],
        '【!谨慎使用】CommonJS 模块加载，建议使用 ES Modules 模块加载代替',
        outputList
      );
    }
  }
}
module.exports = SrJs;
